/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.server.virtual.model.sax;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import javax.jcr.RepositoryException;
import offset.nodes.Constants;
import offset.nodes.client.model.DummyContentHandler;
import offset.nodes.client.model.StringArrayConverter;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

/**
 * This SAXContentSource will glue together multiple content sources to a single document, as seen by the final 
 * accepting content handler.
 * 
 * In addition, each of the component content source may be wrapped into an artificial element provided as a "prefix" 
 * in the constructor 
 * 
 * @author Walter Luetgenau
 */
public class ConcatenatorContentSource implements SAXContentSource {

    UpdatableSAXContentSource[] sources;
    String[] prefixes;
    ContentHandler target;
    Attributes empty = new AttributesImpl();
    ConcatenatorFilter filter;

    public ConcatenatorContentSource(UpdatableSAXContentSource[] sources, String[] prefixes) throws RepositoryException {
        if (sources.length != prefixes.length)
            throw new RepositoryException("Invalid argument");
        this.sources = sources;
        this.prefixes = prefixes;
    }

    /**
     * Filters document events. This will allow the concatenator content source to glue multiple documents together
     */
    class ConcatenatorFilter extends DummyContentHandler implements ContentHandler {

        ContentHandler target;
        List<ExpandedQuery> queries = new ArrayList<ExpandedQuery>();
        boolean primaryProcessed = false;
        Stack<String> path = new Stack<String>();

        public String getQuery(int i) {
            String query = queries.get(i).getExpanded();
            if (query == null)
                query = queries.get(i).getOriginal();

            return query;
        }

        public String getPath(int i) {
            return queries.get(i).getPath();
        }

        class ExpandedQuery {

            public String PRIMARY = Constants.QUERY_PRIMARY + "(";
            public String ELEMENT = "element(*,";
            String original;
            String expanded;

            public ExpandedQuery(String original) {
                this.original = original;
                this.expanded = null;
            }

            public String getOriginal() {
                return original;
            }

            public String getExpanded() {
                return expanded;
            }

            public String getPath() {
                if (expanded == null)
                    return null;
                return getPath(expanded);
            }

            public void setExpanded(String expanded) {
                this.expanded = expanded;
            }

            protected String substituteArray(String input, String[] values) {
                int startPrimary = input.indexOf(PRIMARY);
                int start = input.lastIndexOf("@", startPrimary);
                int end = input.indexOf("=", start);

                String propertyName = input.substring(start, end);

                StringBuffer buf = new StringBuffer();
                buf.append("(");
                for (int i = 0; i < values.length; i++) {
                    if (i > 0)
                        buf.append(" or ");
                    buf.append(propertyName + "='" + values[i] + "'");
                }
                buf.append(")");

                end = input.indexOf("'", startPrimary) + 1;
                return input.substring(0, start) + buf.toString() + input.substring(end);
            }

            public String substitute(String input, String value) {
                if (StringArrayConverter.isArray(value)) {
                    StringArrayConverter converter = new StringArrayConverter(value);

                    return substituteArray(input, converter.toArray());
                } else {
                    int start = input.indexOf(PRIMARY);
                    return input.substring(0, start) + value + input.substring(input.indexOf(")", start) + 1);
                }
            }

            protected String substituteAttributes(String input, Attributes atts) {
                String[] parts = getAttributeConstraint(input).split("=");
                if (parts.length < 2)
                    return input;

                String argument = getArgument(parts[1]);
                if (argument.startsWith("@"))
                    argument = argument.substring(1);
                String value = atts.getValue(argument);
                if (value == null)
                    return input;

                return substitute(input, value);
            }

            protected String getPath(String query) {
                int filter = query.lastIndexOf("/");
                if (filter < 0)
                    return query;
                return query.substring(0, filter);
            }

            protected String getConstraint(String query) {
                int filter = query.lastIndexOf("/");
                if (filter < 0)
                    return query;
                return query.substring(filter + 1);
            }

            protected String getAttributeConstraint(String query) {
                int start = query.indexOf("[");
                int end = query.indexOf("]");
                if (start < 0 || end < 0)
                    return query;
                return query.substring(start + 1, end);
            }

            protected String getArgument(String method) {
                int start = method.indexOf("(");
                int end = method.indexOf(")");
                if (start < 0 || end < 0)
                    return method;
                return method.substring(start + 1, end);
            }

            protected String substitutePath(String input, Stack<String> basis) {
                StringBuffer result = new StringBuffer();

                String path = getPath(input);
                String[] folders = path.split("/");
                Stack<String> newPath = new Stack<String>();
                for (String folder : folders) {
                    if (folder.startsWith(PRIMARY) && getArgument(folder).equals("PATH"))
                        for (int i = 0; i < basis.size(); i++) {
                            newPath.push(basis.get(i));
                        }
                    else if (folder.equals(".."))
                        newPath.pop();
                    else if (folder.equals(".")); else
                        newPath.push(folder);
                }

                for (int i = 0; i < newPath.size(); i++) {
                    String folder = newPath.get(i);
                    if (i > 0)
                        result.append("/");
                    result.append(folder);
                }

                return result.toString() + "//" + getConstraint(input);
            }

            public void substitute(Stack<String> path, Attributes atts) {
                String query = original;
                if (expanded != null)
                    query = expanded;

                if (path != null)
                    expanded = substitutePath(query, path);
                expanded = substituteAttributes(expanded, atts);
            }
        }

        public ConcatenatorFilter(Stack<String> path, ContentHandler target) {
            this.path = (Stack<String>) path.clone();
            this.target = target;

            for (int i = 0; i < sources.length; i++) {
                this.queries.add(new ExpandedQuery(sources[i].getQueryString()));
            }
        }

        public String getName(String uri, String localName, String qName) {
            return qName;
        }

        @Override
        public void startPrefixMapping(String prefix, String uri) throws SAXException {
            target.startPrefixMapping(prefix, uri);
        }

        protected String toPathName(String attName) {
            StringBuffer buf = new StringBuffer();

            for (int i = 0; i < path.size(); i++) {
                if (i > 0)
                    buf.append("/");
                buf.append(path.get(i));
            }
            buf.append("@" + attName);

            return buf.toString();
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
            if (!primaryProcessed) {
                path.push(getName(uri, localName, qName));
                for (int j = 0; j < queries.size(); j++) {
                    queries.get(j).substitute(path, atts);
                }
            }
            target.startElement(uri, localName, qName, atts);
        }

        @Override
        public void skippedEntity(String name) throws SAXException {
            target.skippedEntity(name);
        }

        @Override
        public void setDocumentLocator(Locator locator) {
            target.setDocumentLocator(locator);
        }

        @Override
        public void processingInstruction(String target, String data) throws SAXException {
            this.target.processingInstruction(target, data);
        }

        @Override
        public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
            target.ignorableWhitespace(ch, start, length);
        }

        @Override
        public void endPrefixMapping(String prefix) throws SAXException {
            target.endPrefixMapping(prefix);
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            if (!primaryProcessed)
                path.pop();
            if (path.size() == 0 && !primaryProcessed)
                primaryProcessed = true;
            target.endElement(uri, localName, qName);
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            target.characters(ch, start, length);
        }
    }

    public void setContentHandler(ContentHandler target) {
        this.target = target;
        Stack<String> path = null;
        if (sources[0] instanceof AbstractContentSource)
            path = ((AbstractContentSource) sources[0]).getPath();
        filter =
                new ConcatenatorFilter(path, target);

        for (int i = 0; i < sources.length; i++) {
            sources[i].setContentHandler(filter);
        }
    }

    public void writeContent() throws RepositoryException, SAXException {
        target.startDocument();

        for (int i = 0; i < sources.length; i++) {
            if (sources[i] == null)
                continue;

            if (prefixes[i] != null && (i == 0 || !prefixes[i].equals(prefixes[i - 1]))) {
                if (i > 0 && prefixes[i - 1] != null)
                    target.endElement("", prefixes[i - 1], "");
                target.startElement("", prefixes[i], "", empty);
            }

            sources[i].setQueryString(filter.getQuery(i));
            if (filter.getPath(i) != null && sources[i] instanceof AbstractContentSource)
                ((AbstractContentSource) sources[i]).setPath(filter.getPath(i));
            sources[i].writeContent();
        }

        if (prefixes[prefixes.length - 1] != null && prefixes[prefixes.length - 1].length() > 0)
            target.endElement("", prefixes[prefixes.length - 1], "");

        target.endDocument();
    }
}
